﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Discovery;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Routing;
using EasyComm.Contracts;
using EasyComm.Discovery;
using EasyComm.Hosting.Configuration;
using EasyComm.ServiceUnit;
using EasyComm.Utilities;

namespace EasyComm.Hosting.Extension
{
    internal class NLBDiscoveryExtension : IExtension<ServiceHostBase>, IDisposable
    {
        private NLBRouterServiceHost _owner;
        private RoutingConfiguration _routerConfiguration = new RoutingConfiguration();
        private Uri _discoveryProxyAddress;
        private Type[] _workerContracts;
        private Type[] _slbAdminContracts;

        public NLBDiscoveryExtension(Type[] workerContracts, Type[] slbAdminContracts, Uri discoveryProxyAddress)
        {
            _discoveryProxyAddress = discoveryProxyAddress;
            _workerContracts = workerContracts;
            _slbAdminContracts = slbAdminContracts;
        }

        void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)
        {
            _owner = owner as NLBRouterServiceHost;
            PopulateFromManagedDiscovery();
            ListenToAnnouncements();
        }

        void IExtension<ServiceHostBase>.Detach(ServiceHostBase owner)
        {
            this.Dispose();
        }

        public void Dispose()
        {
        }

        private void PopulateFromManagedDiscovery()
        {
            if (!NetUtils.TCPOnlineCheck(_discoveryProxyAddress.Host, _discoveryProxyAddress.Port))
            {
                Trace("Proxy server is offline ('{0}'). Bypassing managed discovery search.", _discoveryProxyAddress);
                return;
            }

            var binding = GenericConfiguration.GetAnnouncementBinding();

            DiscoveryEndpoint discoveryEndpoint = new DiscoveryEndpoint(binding, new EndpointAddress(_discoveryProxyAddress));
            DiscoveryClient discoveryClient = new DiscoveryClient(discoveryEndpoint);
            List<Type> types = new List<Type>(_workerContracts);
            types.AddRange(_slbAdminContracts);
            var results = discoveryClient.Find(new ServiceDiscoveryCriteria(types.ToArray()));
            //var results = discoveryClient.Find(new FindCriteria(typeof(IAdminSLBContract)));

            foreach (var endpoint in results.Endpoints)
            {
                AddEndpointToRoutingTable(endpoint);
            }
            discoveryClient.Close();
        }

        /// <summary>
        /// Update the routing table based on UDB announcement
        /// </summary>
        private void ListenToAnnouncements()
        {
            AnnouncementService announcementService = new AnnouncementService();
            announcementService.OnlineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(ServiceOnlineEvent);
            announcementService.OfflineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(ServiceOffLineEvent);
            ServiceHost announcementServiceHost = new ServiceHost(announcementService);
            try
            {
                announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
                announcementServiceHost.Open();
            }
            catch (CommunicationException communicationException)
            {
                throw new FaultException("Can't listen to notification of services " + communicationException.Message);
            }
            catch (TimeoutException timeoutException)
            {
                throw new FaultException("Timeout trying to open the notification service " + timeoutException.Message);
            }
        }

        /// <summary>
        /// Fires when a service is online
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ServiceOffLineEvent(object sender, AnnouncementEventArgs e)
        {
            Trace("Endpint offline detected: {0}", e.EndpointDiscoveryMetadata.Address);
            RemoveEndpointFromRoutingTable(e.EndpointDiscoveryMetadata);
        }

        /// <summary>
        /// Fires when a service goes offline
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ServiceOnlineEvent(object sender, AnnouncementEventArgs e)
        {
            Trace("Endpoint online detected: {0}", e.EndpointDiscoveryMetadata.Address);
            AddEndpointToRoutingTable(e.EndpointDiscoveryMetadata);
        }

        private void AddEndpointToRoutingTable(EndpointDiscoveryMetadata endpointMetadata)
        {
            if (endpointMetadata.ContractTypeNames.Count(item => item.Name.Contains("IMetadataExchange")) > 0)
            {
                Trace("Bypassing metadata online announcement url: '{0}'.", endpointMetadata.Address.Uri);
                return;
            }

            Binding binding = GenericConfiguration.GetMainBinding();
            EndpointAddress address = endpointMetadata.Address;

            // set the contract
            var routerContract = ContractDescription.GetContract(typeof(IRequestReplyRouter));
            ServiceEndpoint endpoint = new ServiceEndpoint(routerContract, binding, address);

            bool workerAdded = false;
            bool slbAdded = false;
            if (endpointMetadata.ContractTypeNames.Count(XmlName => _workerContracts.Count(type => type.Name == XmlName.Name) > 0) > 0)
            {
                //worker endpoint
                WorkerServiceUnit workerUnit = new WorkerServiceUnit(_owner) { Status = ServiceStatus.Online, Host = address.Uri.DnsSafeHost, Port = address.Uri.Port };
                for (int i = 0; i < workerUnit.Contracts.Count; i++)
                {
                    workerUnit.Contracts[i].Endpoint = endpoint;
                    workerUnit.Contracts[i].ListenUri = address.Uri;
                }
                this._owner.Owner._workerServiceList.Add(workerUnit);
                Trace("Worker endpoint of type added: {0}", endpointMetadata.Address);
                workerAdded = true;
            }
            if (endpointMetadata.ContractTypeNames.Count(XmlName => _slbAdminContracts.Count(type => type.Name == XmlName.Name) > 0) > 0)
            {
                //slb endpoint
                AdminServiceUnit<IAdminSLBContract> slbAdminUnit = new AdminServiceUnit<IAdminSLBContract>(_owner) { Status = ServiceStatus.Online, Type = AdminServiceType.SLBAdmin, Host = address.Uri.DnsSafeHost, Port = address.Uri.Port };
                this._owner.Owner._slbAdminServiceList.Add(slbAdminUnit);
                Trace("Endpoint of type {0} added: {1}", slbAdminUnit.Type, endpointMetadata.Address);
                slbAdded = true;
            }
            if (!workerAdded && !slbAdded)
            {
                Trace("Service does not implement any of the registered contracts. Bypassing...");
                return;
            }

            if (workerAdded)
            {
                _routerConfiguration.FilterTable.Clear();
                _routerConfiguration.FilterTable.Add(new MatchAllMessageFilter(), this._owner.Owner._workerServiceList.EndpointList);
                this._owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(_routerConfiguration);
            }

        }

        private void RemoveEndpointFromRoutingTable(EndpointDiscoveryMetadata endpointMetadata)
        {
            if (endpointMetadata.ContractTypeNames.Count(item => item.Name.Contains("IMetadataExchange")) > 0)
            {
                Trace("Bypassing metadata offline announcement url: '{0}'.", endpointMetadata.Address.Uri);
                return;
            }

            WorkerServiceUnit workerUnit = this._owner.Owner._workerServiceList[endpointMetadata.Address.Uri];
            if (workerUnit != null)
            {
                this._owner.Owner._workerServiceList.Remove(workerUnit);
                Trace("Worker Endpoint removed: {0}", endpointMetadata.Address);
                _routerConfiguration.FilterTable.Clear();
                if (this._owner.Owner._workerServiceList.Count > 0)
                {
                    _routerConfiguration.FilterTable.Add(new MatchAllMessageFilter(), this._owner.Owner._workerServiceList.EndpointList);
                }
                this._owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(_routerConfiguration);
            }
            else
            {
                AdminServiceUnit<IAdminSLBContract> slbAdminUnit = this._owner.Owner._slbAdminServiceList[endpointMetadata.Address.Uri];
                if (slbAdminUnit != null)
                {
                    this._owner.Owner._slbAdminServiceList.Remove(slbAdminUnit);
                    Trace("SLB Endpoint removed: {0}", endpointMetadata.Address);
                }
            }
        }

        private void Trace(string msg, params object[] args)
        {
            System.Diagnostics.Trace.WriteLine(string.Format(msg, args));
        }
    }
}


